/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Element } from '../nodes/Element';
import List from '@ohos.util.List';
// import { FormElement } from '../nodes/FormElement';
import { StringUtil } from '../internal/StringUtil';
import wantAgent from '@ohos.wantAgent';
import { Validate } from '../helper/Validate';
import { Selector } from './Selector';
import { QueryParser } from './QueryParser';
import HashSet from '@ohos.util.HashSet';
import { NodeVisitor } from './NodeVisitor';
import { NodeFilter } from './NodeFilter';
import { Comment } from '../nodes/Comment';
import { TextNode } from '../nodes/TextNode';
import { Node } from '../nodes/Node';
import { DataNode } from '../nodes/DataNode';
import { FilterResult } from './FilterResult';

export class Elements extends Array<Element> {
  constructor();

  constructor(elements: List<Element>);

  constructor(elements: Element[]);

  constructor(elements: Element);

  constructor(elements?: List<Element> | Element[] | Element) {
    super()
    if (elements !== undefined && elements !== null) {
      if (elements instanceof Element) {
        this.add(elements)
      } else {
        elements.forEach((element, index) => {
          this.add(element)
        })
      }
    }
  }

  public add(element:Element) {
    this.push(element)
  }

  public get(index:number):Element {
    return this[index]
  }

  public isEmpty():boolean {
    return this.length === 0
  }

  /**
   * Creates a deep copy of these elements.
   * @return a deep copy
   */
  public clone(): Elements {
    let clone = new Elements()
    for (let e of this) {
      clone.add(e.clone())
    }
    return clone
  }

  /**
   Get an attribute value from the first matched element that has the attribute.
   @param attributeKey The attribute key.
   @return The attribute value from the first matched element that has the attribute.. If no elements were matched (isEmpty() == true),
   or if the no elements have the attribute, returns empty string.
   @see #hasAttr(String)
   */
  public attr(attributeKey: string): string {
    for (let element of this) {
      if (element.hasAttr(attributeKey)) {
        return element.getAttr(attributeKey)
      }
    }
    return ''
  }

  /**
   Checks if any of the matched elements have this attribute defined.
   @param attributeKey attribute key
   @return true if any of the elements have the attribute; false if none do.
   */
  public hasAttr(attributeKey: string): boolean {
    for (let element of this) {
      if (element.hasAttr(attributeKey)) {
        return true
      }
    }
    return false
  }

  /**
   * Get the attribute value for each of the matched elements. If an element does not have this attribute, no value is
   * included in the result set for that element.
   * @param attributeKey the attribute name to return values for. You can add the {@code abs:} prefix to the key to
   * get absolute URLs from relative URLs, e.g.: {@code doc.select("a").eachAttr("abs:href")} .
   * @return a list of each element's attribute value for the attribute
   */
  public eachAttr(attributeKey: string): List<string> {
    let attrs = new List<string>()
    for (let element of this) {
      if (element.hasAttr(attributeKey)) {
        attrs.add(element.getAttr(attributeKey))
      }
    }
    return attrs
  }

  /**
   * Set an attribute on all matched elements.
   * @param attributeKey attribute key
   * @param attributeValue attribute value
   * @return this
   */
  public setAttr(attributeKey: string, attributeValue: string): Elements {
    for (let element of this) {
      element.setAttr(attributeKey, attributeValue)
    }
    return this
  }

  /**
   * Remove an attribute from every matched element.
   * @param attributeKey The attribute to remove.
   * @return this (for chaining)
   */
  public removeAttr(attributeKey: string): Elements {
    for (let element of this) {
      element.removeAttr(attributeKey)
    }
    return this
  }

  /**
   Add the class name to every matched element's {@code class} attribute.
   @param className class name to add
   @return this
   */
  public addClass(className: string): Elements {
    for (let element of this) {
      element.addClass(className)
    }
    return this
  }

  /**
   Remove the class name from every matched element's {@code class} attribute, if present.
   @param className class name to remove
   @return this
   */
  public removeClass(className: string): Elements {
    for (let element of this) {
      element.removeClass(className)
    }
    return this
  }

  /**
   Toggle the class name on every matched element's {@code class} attribute.
   @param className class name to add if missing, or remove if present, from every element.
   @return this
   */
  public toggleClass(className: string): Elements {
    for (let element of this) {
      element.toggleClass(className)
    }
    return this
  }

  /**
   Determine if any of the matched elements have this class name set in their {@code class} attribute.
   @param className class name to check for
   @return true if any do, false if none do
   */
  public hasClass(className: string): boolean {
    for (let element of this) {
      if (element.hasClass(className)) {
        return true
      }
    }
    return false
  }

  /**
   * Get the form element's value of the first matched element.
   * @return The form element's value, or empty if not set.
   * @see Element#val()
   */
  public getVal(): string {
    if (this.length > 0) {
      return this.first().getVal()
    } else {
      return ''
    }
  }

  /**
   * Set the form element's value in each of the matched elements.
   * @param value The value to set into each matched element
   * @return this (for chaining)
   */
  public setVal(value: string): Elements {
    for (let element of this) {
      element.setVal(value)
    }
    return this
  }

  /**
   * Get the combined text of all the matched elements.
   * <p>
   * Note that it is possible to get repeats if the matched elements contain both parent elements and their own
   * children, as the Element.text() method returns the combined text of a parent and all its children.
   * @return string of all text: unescaped and no HTML.
   * @see Element#text()
   * @see #eachText()
   */
  public text(): string {
    let sb = StringUtil.borrowBuilder()
    for (let element of this) {
      if (sb.length !== 0) {
        sb.push(' ')
      }
      sb.push(element.text())
    }
    return StringUtil.releaseBuilder(sb)
  }

  /**
   Test if any matched Element has any text content, that is not just whitespace.
   @return true if any element has non-blank text content.
   @see Element#hasText()
   */
  public hasText(): boolean {
    for (let element of this) {
      if (element.hasText()) {
        return true
      }
    }
    return false
  }

  /**
   * Get the text content of each of the matched elements. If an element has no text, then it is not included in the
   * result.
   * @return A list of each matched element's text content.
   * @see Element#text()
   * @see Element#hasText()
   * @see #text()
   */
  public eachText(): List<string> {
    let texts = new List<string>()
    for (let element of this) {
      if (element.hasText()) {
        texts.add(element.text())
      }
    }
    return texts
  }

  /**
   * Get the combined inner HTML of all matched elements.
   * @return string of all element's inner HTML.
   * @see #text()
   * @see #outerHtml()
   */
  public html(): string {
    let sb = StringUtil.borrowBuilder()
    for (let element of this) {
      if (sb.length !== 0) {
        sb.push('\n')
      }
      sb.push(element.getHtml())
    }
    return StringUtil.releaseBuilder(sb)
  }

  /**
   * Get the combined outer HTML of all matched elements.
   * @return string of all element's outer HTML.
   * @see #text()
   * @see #html()
   */
  public outerHtml(): string {
    let sb = StringUtil.borrowBuilder()
    for (let element of this) {
      if (sb.length !== 0) {
        sb.push('\n')
      }
      sb.push(element.outerHtml())
    }
    return StringUtil.releaseBuilder(sb)
  }

  /**
   * Get the combined outer HTML of all matched elements. Alias of {@link #outerHtml()}.
   * @return string of all element's outer HTML.
   * @see #text()
   * @see #html()
   */
  public toString(): string {
    return this.outerHtml()
  }

  /**
   * Update (rename) the tag name of each matched element. For example, to change each {@code <i>} to a {@code <em>}, do
   * {@code doc.select("i").tagName("em");}
   *
   * @param tagName the new tag name
   * @return this, for chaining
   * @see Element#tagName(String)
   */
  public tagName(tagName: string): Elements {
    for (let element of this) {
      element.setTagName(tagName)
    }
    return this
  }

  /**
   * Set the inner HTML of each matched element.
   * @param html HTML to parse and set into each matched element.
   * @return this, for chaining
   * @see Element#html(String)
   */
  public setHtml(html: string): Elements {
    for (let element of this) {
      element.setHtml(html)
    }
    return this
  }

  /**
   * Add the supplied HTML to the start of each matched element's inner HTML.
   * @param html HTML to add inside each element, before the existing HTML
   * @return this, for chaining
   * @see Element#prepend(String)
   */
  public prepend(html: string): Elements {
    for (let element of this) {
      element.prepend(html)
    }
    return this
  }

  /**
   * Add the supplied HTML to the end of each matched element's inner HTML.
   * @param html HTML to add inside each element, after the existing HTML
   * @return this, for chaining
   * @see Element#append(String)
   */
  public append(html: string): Elements {
    for (let element of this) {
      element.append(html)
    }
    return this
  }

  /**
   * Insert the supplied HTML before each matched element's outer HTML.
   * @param html HTML to insert before each element
   * @return this, for chaining
   * @see Element#before(String)
   */
  public before(html: string): Elements {
    for (let element of this) {
      element.before(html)
    }
    return this
  }

  /**
   * Insert the supplied HTML after each matched element's outer HTML.
   * @param html HTML to insert after each element
   * @return this, for chaining
   * @see Element#after(String)
   */
  public after(html: string): Elements {
    for (let element of this) {
      element.after(html)
    }
    return this
  }

  /**
   Wrap the supplied HTML around each matched elements. For example, with HTML
   {@code <p><b>This</b> is <b>Jsoup</b></p>},
   <code>doc.select("b").wrap("&lt;i&gt;&lt;/i&gt;");</code>
   becomes {@code <p><i><b>This</b></i> is <i><b>jsoup</b></i></p>}
   @param html HTML to wrap around each element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
   @return this (for chaining)
   @see Element#wrap
   */
  public wrap(html: string): Elements {
    Validate.notEmpty(html,'Elements wrap html must not be empty')
    for (let element of this) {
      element.wrap(html)
    }
    return this
  }

  /**
   * Removes the matched elements from the DOM, and moves their children up into their parents. This has the effect of
   * dropping the elements but keeping their children.
   * <p>
   * This is useful for e.g removing unwanted formatting elements but keeping their contents.
   * </p>
   *
   * E.g. with HTML: <p>{@code <div><font>One</font> <font><a href="/">Two</a></font></div>}</p>
   * <p>{@code doc.select("font").unwrap();}</p>
   * <p>HTML = {@code <div>One <a href="/">Two</a></div>}</p>
   *
   * @return this (for chaining)
   * @see Node#unwrap
   */
  public unwrap(): Elements {
    for (let element of this) {
      element.unwrap()
    }
    return this
  }

  /**
   * Empty (remove all child nodes from) each matched element. This is similar to setting the inner HTML of each
   * element to nothing.
   * <p>
   * E.g. HTML: {@code <div><p>Hello <b>there</b></p> <p>now</p></div>}<br>
   * <code>doc.select("p").empty();</code><br>
   * HTML = {@code <div><p></p> <p></p></div>}
   * @return this, for chaining
   * @see Element#empty()
   * @see #remove()
   */
  public empty(): Elements {
    for (let element of this) {
      element.empty()
    }
    return this
  }

  /**
   * Remove each matched element from the DOM. This is similar to setting the outer HTML of each element to nothing.
   * <p>
   * E.g. HTML: {@code <div><p>Hello</p> <p>there</p> <img /></div>}<br>
   * <code>doc.select("p").remove();</code><br>
   * HTML = {@code <div> <img /></div>}
   * <p>
   * Note that this method should not be used to clean user-submitted HTML; rather, use {@link org.jsoup.safety.Cleaner} to clean HTML.
   * @return this, for chaining
   * @see Element#empty()
   * @see #empty()
   */
  public removeElements(): Elements {
    for (let element of this) {
      element.remove()
    }
    return this
  }

  /**
   * Find matching elements within this element list.
   * @param query A {@link Selector} query
   * @return the filtered list of elements, or an empty list if none match.
   */
  public select(query: string): Elements {
    // @ts-ignore
    return Selector.select(query, this)
  }

  /**
   * Remove elements from this list that match the {@link Selector} query.
   * <p>
   * E.g. HTML: {@code <div class=logo>One</div> <div>Two</div>}<br>
   * <code>Elements divs = doc.select("div").not(".logo");</code><br>
   * Result: {@code divs: [<div>Two</div>]}
   * <p>
   * @param query the selector query whose results should be removed from these elements
   * @return a new elements list that contains only the filtered results
   */
  public not(query: string): Elements {
    // @ts-ignore
    let out = Selector.select(query, this)
    // @ts-ignore
    return Selector.filterOut(this, out)
  }

  /**
   * Get the <i>nth</i> matched element as an Elements object.
   * <p>
   * See also {@link #get(int)} to retrieve an Element.
   * @param index the (zero-based) index of the element in the list to retain
   * @return Elements containing only the specified element, or, if that element did not exist, an empty list.
   */
  public eq(index: number): Elements {
    return this.length > index ? new Elements(this.get(index)) : new Elements()
  }

  /**
   * Test if any of the matched elements match the supplied query.
   * @param query A selector
   * @return true if at least one element in the list matches the query.
   */
  public is(query: string): boolean {
    let evaluator = QueryParser.parse(query)
    for (let e of this) {
      if (e.is(evaluator)) {
        return true
      }
    }
    return false
  }

  /**
   * Get the immediate next element sibling of each element in this list.
   * @return next element siblings.
   */
  public next(query: string | null = null): Elements {
    return this.siblings(query, true, false)
  }

  /**
   * Get each of the following element siblings of each element in this list.
   * @return all following element siblings.
   */
  public nextAll(query: string | null = null): Elements {
    return this.siblings(query, true, true)
  }

  /**
   * Get the immediate previous element sibling of each element in this list, filtered by the query.
   * @param query CSS query to match siblings against
   * @return previous element siblings.
   */
  public prev(query: string | null = null): Elements {
    return this.siblings(query, false, false)
  }

  /**
   * Get each of the previous element siblings of each element in this list, that match the query.
   * @param query CSS query to match siblings against
   * @return all previous element siblings.
   */
  public prevAll(query: string | null = null): Elements {
    return this.siblings(query, false, true)
  }

  private siblings(query: string, next: boolean, all: boolean): Elements {
    let els = new Elements()
    let evaluator = (query !== undefined && query !== null) ? QueryParser.parse(query) : null
    for (let e of this) {
      do {
        let sib = next ? e.nextElementSibling() : e.previousElementSibling()
        if (sib === undefined || sib === null) {
          break
        }
        if (evaluator === undefined || evaluator === null) {
          els.add(sib)
        } else if (sib.is(evaluator)) {
          els.add(sib)
        }
        e = sib
      } while (all)
    }
    return els
  }

  /**
   * Get all of the parents and ancestor elements of the matched elements.
   * @return all of the parents and ancestor elements of the matched elements
   */
  public parents(): Elements {
    let combo = new HashSet<Element>()
    for (let e of this) {
      let eps = e.parents()
      for (let ep of eps) {
        combo.add(ep)
      }
    }
    let list = new List<Element>()
    combo.forEach((value, index) => {
      list.add(value)
    })
    return new Elements(list)
  }

  /**
   Get the first matched element.
   @return The first matched element, or <code>null</code> if contents is empty.
   */
  public first(): Element {
    return this.isEmpty() ? null : this.get(0)
  }

  /**
   Get the last matched element.
   @return The last matched element, or <code>null</code> if contents is empty.
   */
  public last(): Element {
    return this.isEmpty() ? null : this.get(this.length - 1)
  }

  /**
   * Perform a depth-first traversal on each of the selected elements.
   * @param nodeVisitor the visitor callbacks to perform on each node
   * @return this, for chaining
   */
  public traverse(nodeVisitor: NodeVisitor): Elements {
    Validate.notNull(nodeVisitor)
    for (let el of this) {
      el.traverse(nodeVisitor, el)
    }
    return this
  }

  /**
   * Perform a depth-first filtering on each of the selected elements.
   * @param nodeFilter the filter callbacks to perform on each node
   * @return this, for chaining
   */
  public filterElements(nodeFilter: NodeFilter): Elements {
    Validate.notNull(nodeFilter)
    for (let el of this) {
      if (el.filterNode(nodeFilter, el) === FilterResult.STOP) {
        break
      }
    }
    return this
  }

  /**
   * Get the {@link FormElement} forms from the selected elements, if any.
   * @return a list of {@link FormElement}s pulled from the matched elements. The list will be empty if the elements contain
   * no forms.
   */
  // public forms(): List<FormElement> {
  //   let forms = new List<FormElement>()
  //   for (let el of this) {
  //     if (el instanceof FormElement) {
  //       forms.add(el)
  //     }
  //   }
  //   return forms
  // }

  /**
   * Get {@link Comment} nodes that are direct child nodes of the selected elements.
   * @return Comment nodes, or an empty list if none.
   */
  public comments(): List<Node> {
    return this.childNodesOfType(1) as List<Node>
  }

  /**
   * Get {@link TextNode} nodes that are direct child nodes of the selected elements.
   * @return TextNode nodes, or an empty list if none.
   */
  public textNodes(): List<TextNode> {
    return this.childNodesOfType(2) as List<TextNode>
  }

  /**
   * Get {@link DataNode} nodes that are direct child nodes of the selected elements. DataNode nodes contain the
   * content of tags such as {@code script}, {@code style} etc and are distinct from {@link TextNode}s.
   * @return Comment nodes, or an empty list if none.
   */
  public dataNodes(): List<DataNode> {
    return this.childNodesOfType(3) as List<DataNode>
  }

  private childNodesOfType(type: number): List<Node> {
    let nodes = new List<Node>()
    for (let el of this) {
      for (let index = 0; index < el.childNodeSize(); index++) {
        let node = el.childNode(index)
        switch (type) {
          case 1:
            if (node instanceof Comment) {
              nodes.add(node)
            }
            break
          case 2:
            if (node instanceof TextNode) {
              nodes.add(node)
            }
            break
          case 3:
            if (node instanceof DataNode) {
              nodes.add(node)
            }
            break
        }
      }
    }
    return nodes
  }
}

